module Bitmex

import HTTP
import JSON
using Formatting
using Dates
using Lazy
using ..Helpers

import Reactive; const R = Reactive

import ..Model
const PriceType = Model.PriceType
const Bucket = Model.Bucket
const CandleStickChart = Model.CandleStickChart
const Trade = Model.Trade

include("ws.jl")
import .WS

const TradeOrNothing = Union{Trade, Nothing}
const BucketOrNothing = Union{Bucket, Nothing}

function get_history_only(symbol :: String,
                          interval :: Period,
                          count :: Int
                         ) :: Vector{Bucket}

  @assert count > 0

  interval_dict = Dict(Model.BitmexOneMinute => "1m",
                       Model.BitmexFiveMinutes => "5m",
                       Model.BitmexOneHour => "1h",
                       Model.BitmexOneDay => "1d")

  raw_interval = interval_dict[interval]

  endpoint = "/trade/bucketed"
  query = Dict("binSize"=>raw_interval,
               "partial"=>"true",
               "symbol"=>symbol,
               "count"=>count,
               "reverse"=>"true"
               # reversinti reikia nes kitaip gauni metu senumo bucketus
              )
  url = HTTP.URI(;
                 scheme="https",
                 host="www.bitmex.com",
                 path="/api/v1" * endpoint,
                 query=query)
  r = HTTP.get(url, ["Accept" => "application/json"],
               retry=false, redirect=false)
  json = JSON.parse(String(r.body))
  # kad bucketai prasidetu nuo seniausio gauto
  reversed_json = Iterators.reverse(json)
  buckets = map(reversed_json) do dict
    Bucket(dict["open"],
           dict["high"],
           dict["low"],
           dict["close"],
           WS.parse_bx_datetime(dict["timestamp"]),
           interval
          )
  end
  buckets
end

function apply_to_bucket(bucket :: Bucket, trade :: Trade) :: Bucket
  close = trade.price
  high = max(bucket.high, close)
  low = min(bucket.low, close)
  open = bucket.open
  end_time = bucket.end_time # bucket end time
  interval = bucket.interval
  Bucket(open,high,low,close,end_time,interval)
end

function before_bucket_end(bucket :: Bucket, trade :: Trade) :: Bool
  bucket.end_time > trade.time
end

function calc_next_end(last_partial :: Bucket) :: DateTime
  last_partial.end_time + last_partial.interval
end

function make_new_partial(last_partial :: Bucket, trade :: Trade) :: Bucket
  p = trade.price
  next_end = calc_next_end(last_partial)
  Bucket(p,p,p,p, next_end, last_partial.interval)
end

function update_partial(trade :: TradeOrNothing,
                        partial :: BucketOrNothing) :: BucketOrNothing
  if issome(trade) && issome(partial)
    new_partial = begin
      if before_bucket_end(partial, trade)
        @debug "update_partial: updating"
        apply_to_bucket(partial, trade)
      else
        @debug "update_partial: making a new one"
        make_new_partial(partial, trade)
      end
    end
    if partial != new_partial
      @debug "update_partial: partials differ" partial new_partial
      new_partial
    else
      nothing
    end
  end
end

function process_trades_to_partial!(trade_signal :: R.Signal{TradeOrNothing},
                                    partial_signal :: R.Signal{BucketOrNothing}
                                   )
  push_new_partial!(partial :: Bucket) =
    begin 
      @debug "process_trades_to_partial! pushing new partial" partial
      push!(partial_signal, partial)
    end
 #both_some(a, b) :: Bool = issome(a) && issome(b)
 #both_some(ab) = both_some(ab...)
  alias_update_partial(trade_and_partial) = begin
    trade, partial = trade_and_partial
    update_partial(trade, partial)
  end
  @>> begin
    map(tuple, trade_signal, partial_signal,
        typ = Tuple{TradeOrNothing, BucketOrNothing})
    R.sampleon(trade_signal)
    map(alias_update_partial, typ = BucketOrNothing)
    foreach(when_some(push_new_partial!))
  end
end

function listen_on_partial!(symbol :: String,
                            init_partial :: BucketOrNothing = nothing)
  trade_signal = R.Signal(TradeOrNothing, nothing)
  foreach(debug("trade_signal"),trade_signal)
  @async begin
    try
      WS.feed_trades!(symbol, trade_signal)
    catch e
      @error "uncaught exception while feeding trades" e
    end
  end
  partial_signal = R.Signal(BucketOrNothing, init_partial)
  foreach(debug("partial_signal"), partial_signal)
  process_trades_to_partial!(trade_signal, partial_signal)
  partial_signal
end

const HistoryOrNothing = Union{Vector{Bucket},Nothing}

function split_at_last(vec :: Vector{T}) :: Tuple{Vector{T}, T} where {T}
  len = length(vec)
  @assert len >= 2
  body = vec[1 : len - 1]
  last = vec[len]
  (body, last)
end

function append_old_partial_to_history!(
    buckets_signal :: R.Signal{HistoryOrNothing},
    prev_partial :: BucketOrNothing,
    buckets :: HistoryOrNothing)
  if issome(prev_partial) && issome(buckets)
    add_to_end! = push!
    add_to_end!(buckets, prev_partial)
    push!(buckets_signal, buckets)
  end
end

function get_updating_history(symbol :: String, interval :: Period
                             ) :: R.Signal{HistoryOrNothing}
  buckets = R.Signal(HistoryOrNothing, nothing)
  partial = R.Signal(BucketOrNothing, nothing)
  prev_partial = R.previous(partial)

  foreach((prev_partial, partial) -> begin @debug "" prev_partial partial end,
          prev_partial, partial)

  partial_end_time = begin
    when_some_get_end_time = when_some(x -> x.end_time)
    @>> begin
      partial
      map(when_some_get_end_time, typ = Union{DateTime,Nothing})
      R.droprepeats()
    end
  end
  foreach(debug("partial_end_time"), partial_end_time)
  begin
    alias_append_old_partial_to_history!(prev_p_and_bs) = begin
      prev_p, bs = prev_p_and_bs
      append_old_partial_to_history!(buckets, prev_p, bs)
    end
    @>> begin
      map(tuple, prev_partial, buckets,
          typ = Tuple{BucketOrNothing, HistoryOrNothing})
      R.sampleon(partial_end_time)
      map(doon(debug("R.sampleon(partial_end_time)")), typ = Tuple{BucketOrNothing, HistoryOrNothing})
      foreach(alias_append_old_partial_to_history!)
    end
  end
  @async begin
    try
      _buckets = get_history_only(symbol, interval, 3)
      _buckets, _partial = split_at_last(_buckets)
      push!(buckets, _buckets)
      push!(partial, _partial)
      _aux_partial_signal = listen_on_partial!(symbol, _partial)
      foreach(_aux_partial_signal) do new_partial
        @debug "_aux_partial_signal" new_partial
        if issome(new_partial)
          @debug "_aux_partial_signal: pushing"
          push!(partial, new_partial)
        end
      end
      #R.bind!(partial, _aux_partial_signal, false, initial = false)
    catch e
      @error "async op fail" e
    end
  end
  buckets_and_partial_rejoined =
      map(buckets, partial, typ = HistoryOrNothing) do buckets, partial
    both_some = issome(buckets) && issome(partial)
    if both_some
      push!(buckets, partial) # TODO ar cia mutability ok?
      buckets
    end
  end
  buckets_and_partial_rejoined
end


function debug_updating_history()
  signal = get_updating_history("XBTUSD", Model.BitmexOneMinute)
  print_buckets(history, length) = @debug "buckets" history length
  @>> begin
    signal
    map(when_some(length), typ = Union{Int64, Nothing})
    foreach(print_buckets, signal)
  end
end

function debug_listen_on_partial()
  b = Bucket(0,0,0,0,Dates.now(), Model.BitmexOneMinute)
  signal = listen_on_partial!("XBTUSD", b)
  foreach(debug(""), signal)
end

end
